iT邦幫忙

2024 iThome 鐵人賽

DAY 16
0
Software Development

用 NestJS 闖蕩微服務!系列 第 16

[用NestJS闖蕩微服務!] DAY16 - Logger

  • 分享至 

  • xImage
  •  

微服務的 Log

Log 是系統運作時產生的記錄,可以是系統發生的事件、錯誤訊息或是其他 有助於釐清系統行為與問題的重要線索。一個最簡單、基礎的 Log 可以是由 Node.js 的 Console API 所產生,如下:

console.log('Hello World!');
console.error('Error!');
console.warn('Warning!');

在微服務的架構下 Log 是相當重要的,當服務發生問題時,能夠有效找出問題的方式就是觀察與分析 Log,想像現在有數十、數百個服務,如果沒有有效的 Log 幫助開發者釐清問題,就像是矇著雙眼玩解謎遊戲,解題難度大幅提升,更可能因此造成企業承受損失,導致一場災難的發生。

產生 Log 的方式有很多種,無論採用哪種方式最重要的是 留下易於解析、有效的資訊,在服務數量非常多且需要聚合、分析它們 Log 的情況下,規範 Log 產生的樣式、格式就變得十分重要。

Log Level

一般來說,會依照 Log 的重要程度與使用場景進行等級分類,目前較常見的有六種:

  1. TRACE:最小粒度的 Log,用來追蹤一段程式運作過程的相關資訊,由於產生的 Log 數量會很多且訊息雜亂,一般來說 不會在生產環境中使用
  2. DEBUG:用來幫助開發者釐清內部狀態的相關資訊,由於產生的 Log 數量會很多且訊息雜亂,一般來說會盡量 避免在生產環境中使用
  3. INFO:用來幫助維運人員與開發者釐清運作狀態與事件的相關資訊,在使用上需確保 Log 內容清楚、有意義,避免過多雜訊。
  4. WARN:用來記錄暫時不影響系統運作的異常狀況相關資訊,這等級的 Log 屬於警訊,應該 要在生產環境中忠實呈現
  5. ERROR:用來記錄影響功能運作的錯誤的相關資訊,這等級的 Log 比 WARN 更具急迫性,必須在生產環境中忠實呈現
  6. FATAL:嚴重影響系統運作的錯誤的相關資訊,這等級的 Log 比 ERROR 更具急迫性,必須在生產環境中忠實呈現 且應當立即處理。

目前主流的 Log 框架都有提供上述六種等級的 Log 產生方法, Node.js 生態圈中最常見的框架就是 winstonpino

NestJS Logger

在預設情況下啟動 NestJS 應用程式,會在終端機印出一連串五顏六色的 Log 資訊,這些 Log 是透過 NestJS 內建的 ConsoleLogger 所產生,它是基於文字的 Log 產生器,透過調整排版與色彩來提升可讀性。

NestJS 在 Log 上提供了十足的彈性,不僅可以調整要顯示的 Log Level,甚至還可以讓開發者自行設計 Logger,用 NestJS 的開發風格來整合最符合團隊的 Log 規範。接下來就帶大家來了解一下 NestJS 的 Logger 設計吧!

Log Level

在 NestJS 的架構下,Logger 必須實作 LoggerService 介面,進而規範不同 Log Level 的實作,下方是介面定義的方法名稱:

  • verbose:對應到 TRACE 等級,不是必要的實作。
  • debug:對應到 DEBUG 等級,是必要的實作。
  • log:對應到 INFO 等級,是必要的實作。
  • warn:對應到 WARN 等級,是必要的實作。
  • error:對應到 ERROR 等級,是必要的實作。
  • fatal:對應到 FATAL 等級,不是必要的實作。

不論是內建、自訂或第三方的 Logger 都會依照該介面進行實作,於是 NestJS 實作了一個叫 Logger 的 Helper 來 代表在 NestJS 應用程式套用的 Logger,如此一來,在程式碼中就可以統一使用 Logger 來產生 Log,減少因更換 Logger 而異動的部分。下方是範例程式碼,在 main.ts 使用 Logger 提供的上述六種方法來印出 Log:

import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
  Logger.verbose('TRACE!');
  Logger.debug('DEBUG!');
  Logger.log('INFO!');
  Logger.warn('WARN!');
  Logger.error('ERROR!');
  Logger.fatal('FATAL!');
}
bootstrap();

啟動應用程式後,會在終端機看到六種顏色的 Log:

Build-in Logger Log Level Result1

如果希望只顯示某些 Level 的 Log,可以在 NestFactorycreate 的選項參數中帶入 logger 參數,並以陣列方式指定要顯示的 Log Level。下方是範例程式碼,在 logger 指定顯示 INFO、WARN、ERROR 與 FATAL 四種等級 的 Log:

import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: ['log', 'warn', 'error', 'fatal'],
  });
  await app.listen(3000);
  Logger.verbose('TRACE!');
  Logger.debug('DEBUG!');
  Logger.log('INFO!');
  Logger.warn('WARN!');
  Logger.error('ERROR!');
  Logger.fatal('FATAL!');
}
bootstrap();

此時終端機就只會顯示符合這四種等級的 Log:

Build-in Logger Log Level Result2

如果希望不顯示 Log,可以指派 logger 的值為 false。下方是範例程式碼:

import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: false,
  });
  await app.listen(3000);
  Logger.verbose('TRACE!');
  Logger.debug('DEBUG!');
  Logger.log('INFO!');
  Logger.warn('WARN!');
  Logger.error('ERROR!');
  Logger.fatal('FATAL!');
}
bootstrap();

此時終端機將不會印出任何 Log:

Build-in Logger Log Level Result3

Custom Logger

如果不想使用 ConsoleLogger 的樣式,可以使用前面提到的 LoggerService 介面實作屬於自己的 Logger。下方是範例程式碼,設計了 MyLoggerService 並實作 LoggerService 介面:

import { LoggerService } from '@nestjs/common';

export class MyLoggerService implements LoggerService {
  verbose(message: any, ...optionalParams: any[]) {
    console.trace('TRACE', message, ...optionalParams);
  }

  debug(message: any, ...optionalParams: any[]) {
    console.debug('DEBUG', message, ...optionalParams);
  }

  log(message: any, ...optionalParams: any[]) {
    console.log('LOG', message, ...optionalParams);
  }

  warn(message: any, ...optionalParams: any[]) {
    console.warn('WARN', message, ...optionalParams);
  }

  error(message: any, ...optionalParams: any[]) {
    console.error('ERROR', message, ...optionalParams);
  }

  fatal(message: any, ...optionalParams: any[]) {
    console.error('FATAL', message, ...optionalParams);
  }
}

實作完畢後,可以將實例化的 MyLoggerService 指派給 logger 進而取代 ConsoleLogger

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MyLoggerService } from './logger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: new MyLoggerService(),
  });
  await app.listen(3000);
}
bootstrap();

啟動應用程式後,會在終端機看到 MyLoggerService 定義的 Log 格式:

Custom Logger Result

DI-based Logger

上方 Custom Logger 範例產生出來的 MyLoggerService 實例 沒有 被 NestJS 的 IoC Container 管理,如果沒有拆分實例的必要,這樣的作法不僅需要消耗較多資源,也會變得較難執行單元測試,於是 NestJS 有為此想了一個方式來讓 Custom Logger 也能由 NestJS 管理實例,首先,在 MyLoggerService 加上 @Injectable 裝飾器:

import { LoggerService, Injectable } from '@nestjs/common';

@Injectable()
export class MyLoggerService implements LoggerService {
  // ...
}

接著,建立一個 LoggerModule 並將 MyLoggerService 放入 providers 中,同時將其匯出:

import { Module } from '@nestjs/common';
import { MyLoggerService } from './logger.service';

@Module({
  providers: [MyLoggerService],
  exports: [MyLoggerService],
})
export class LoggerModule {}

補充:此處的內容涉及 Provider 相關知識,針對 NestJS Provider 可以參考官方文件或是我之前分享的文章

AppModule 使用 LoggerModule 以便在應用程式啟動時建立 MyLoggerService 實例:

import { Module } from '@nestjs/common';
import { LoggerModule } from './logger';
// ...

@Module({
  imports: [LoggerModule],
  // ...
})
export class AppModule {}

最後,調整 main.ts 的內容,透過 appget 方法取出 MyLoggerService 實例,並透過 useLogger 方法進行套用,這裡要特別注意,建議在 NestFactorycreate 選項參數中配置 bufferLogstrue,因為建立 NestApplication 實例本身不參與依賴注入的初始化階段,所以在這時候所產生的 Log 依然會使用 ConsoleLogger 輸出,但只要將 bufferLogs 設為 true 就可以先將這些 Log 進行緩衝,直到透過 useLogger 套用的 Logger 生效才會進行輸出,確保格式統一:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MyLoggerService } from './logger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    bufferLogs: true,
  });
  app.useLogger(app.get(MyLoggerService));
  await app.listen(3000);
}
bootstrap();

啟動應用程式後,會在終端機看到 MyLoggerService 定義的 Log 格式:

DI-based Logger Result

擴充 ConsoleLogger

如果只想要調整 ConsoleLogger 部分方法,可以採取擴充的方式來實作,就不需要使用 Custom Logger 的方式重新造輪子。下方是範例程式碼,建立一個 MyConsoleLogger 的類別並繼承 ConsoleLogger,這裡針對 log 的部分進行調整,設計 log 方法並將 message 參數放入物件中由父類別的 log 產生 Log:

import { ConsoleLogger, Injectable } from '@nestjs/common';

@Injectable()
export class MyConsoleLogger extends ConsoleLogger {
  log(message: any, ...optionalParams: [...any, string?]): void {
    super.log({ message }, ...optionalParams);
  }
}

調整 main.ts 的內容,指派 loggerMyConsoleLogger 的實例即可套用:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MyConsoleLogger } from './logger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: new MyConsoleLogger(),
  });
  await app.listen(3000);
}
bootstrap();

當然也可以採用 DI-based 的方式來管理實例:

import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { AppModule } from './app.module';
import { MyConsoleLogger } from './logger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    bufferLogs: true,
  });
  app.useLogger(app.get(MyConsoleLogger));
  await app.listen(3000);
}
bootstrap();

注意:要記得將 MyConsoleLogger 掛在 Module 底下。

啟動應用程式後,會在終端機看到包裝成物件後的訊息:

Extend Console Logger Result

pino Logger

雖然 NestJS 內建的 ConsoleLogger 可以產出容易閱讀的 Log,但在微服務架構下,各個服務如果產生的 Log 都是純文字,就會變得難以解析,所以比較好的 Log 格式可以 JSON 的方式呈現,這樣要做解析會變得容易許多。

前面有提到 Node.js 生態圈中最常見的 Log 框架是 winston 與 pino,這兩個框架都可以產生 JSON 格式的 Log,那該選哪個框架呢?以我個人來說會選擇 pino,原因是 pino 的效能表現較佳

NestJS 社群有針對 pino 進行包裝,提供 nestjs-pino 套件讓 NestJS 開發者使用。透過下方指令安裝相關套件:

$ npm install nestjs-pino pino-http

補充pino-http 是基於 pino 的高效 HTTP Logger,可以針對每一個 HTTP Request 進行相關處理,讓 Log 變得更具有意義,有興趣的朋友可以參考官方文件

套用 Logger

AppModule 使用 nestjs-pino 提供的 LoggerModule,並透過其 forRootforRootAsync 方法產生相關實例:

import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';
// ...

@Module({
  imports: [ LoggerModule.forRoot()],
  // ...
})
export class AppModule {}

接著,調整 main.ts 的內容,透過 DI-based 的方式指定使用 nestjs-pinoLogger

import { NestFactory } from '@nestjs/core';
import { Logger } from 'nestjs-pino';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    bufferLogs: true,
  });
  app.useLogger(app.get(Logger));
  await app.listen(3000);
}
bootstrap();

啟動應用程式後,會在終端機看到 JSON 格式的 Log:

pino logger Result1

開發模式下的 Log

雖然 Log 有正確轉換成 JSON 的格式,但在開發時還是以文字的方式呈現 Log 資訊會更容易閱讀,這時候就可以根據不同環境進行調整,pino 有推出 pino-pretty 來將 Log 轉換成文字格式。透過下方指令進行安裝:

$ npm install pino-pretty

安裝完之後,針對 LoggerModule 的部分進行調整,在 forRoot 帶入相關參數,僅在 NODE_ENV 不是 production 的時候才將 transport 設定為 pino-pretty,並且將 level 設為 debug

import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';
// ...

const isProduction = () => process.env.NODE_ENV === 'production';

@Module({
  imports: [
    LoggerModule.forRoot({
      pinoHttp: {
        level: !isProduction() ? 'debug' : 'info',
        transport: !isProduction() ? { target: 'pino-pretty' } : undefined,
      },
    }),
  ],
  // ...
})
export class AppModule {}

啟動應用程式後,會在終端機看到格式化後的 Log:

pino logger Result2

如果在啟動時設定環境變數 NODE_ENVproduction,終端機顯示的就會是 JSON 格式:

pino logger Result3

小結

回顧一下今天的重點內容,在微服務架構下 Log 是相當重要的,透過有效的 Log 可以幫助我們釐清問題,若將 Log 依照重要程度與種類進行分類,在 Log 的分析、聚合上會更有幫助。NestJS 有內建 ConsoleLogger 來產生文字格式的 Log,甚至有定義出 loggerService 的介面,讓 Custom Logger、ConsoleLogger 使用相同介面進行設計,大幅提升一致性。

雖然 ConsoleLogger 產生的訊息在開發模式下容易閱讀,但在服務很多的情況下,還是以 JSON 格式進行 Log 分析、聚合會更容易,所以可以使用 NestJS 社群提供的 nestjs-pino 來協助產生。

在了解 NestJS 如何產生 Log 之後,下一篇將會介紹在微服務下要如何收集、解析 Log,敬請期待!


上一篇
[用NestJS闖蕩微服務!] DAY15 - Prometheus (下)
下一篇
[用NestJS闖蕩微服務!] DAY17 - ELK 與 Log
系列文
用 NestJS 闖蕩微服務!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言